<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
    />
    <meta name="description" content="Star burst overlapping billboards." />
    <meta name="cesium-sandcastle-labels" content="Showcases" />
    <title>Cesium Demo</title>
    <script type="text/javascript" src="../Sandcastle-header.js"></script>
    <script
      type="text/javascript"
      src="../../../Build/CesiumUnminified/Cesium.js"
      nomodule
    ></script>
    <script type="module" src="../load-cesium-es6.js"></script>
  </head>
  <body
    class="sandcastle-loading"
    data-sandcastle-bucket="bucket-requirejs.html"
  >
    <style>
      @import url(../templates/bucket.css);
    </style>
    <div id="cesiumContainer" class="fullSize"></div>
    <div id="loadingOverlay"><h1>Loading...</h1></div>
    <div id="toolbar">
      <div id="zoomButtons"></div>
    </div>
    <script id="cesium_sandcastle_script">
      function startup(Cesium) {
        "use strict";
        //Sandcastle_Begin
        const viewer = new Cesium.Viewer("cesiumContainer", {
          selectionIndicator: false,
        });

        // Add labels clustered at the same location
        const numBillboards = 30;
        for (let i = 0; i < numBillboards; ++i) {
          const position = Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883);
          viewer.entities.add({
            position: position,
            billboard: {
              image: "../images/facility.gif",
              scale: 2.5,
            },
            label: {
              text: `Label${i}`,
              show: false,
            },
          });
        }

        const scene = viewer.scene;
        const camera = scene.camera;
        const handler = new Cesium.ScreenSpaceEventHandler(scene.canvas);

        handler.setInputAction(function (movement) {
          // Star burst on left mouse click.
          starBurst(movement.position);
        }, Cesium.ScreenSpaceEventType.LEFT_CLICK);

        handler.setInputAction(function (movement) {
          // Remove the star burst when the mouse exits the circle or show the label of the billboard the mouse is hovering over.
          updateStarBurst(movement.endPosition);
        }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

        camera.moveStart.addEventListener(function () {
          // Reset the star burst on camera move because the lines from the center
          // because the line end points rely on the screen space positions of the billboards.
          undoStarBurst();
        });

        // State saved across mouse click and move events
        const starBurstState = {
          enabled: false,
          pickedEntities: undefined,
          billboardEyeOffsets: undefined,
          labelEyeOffsets: undefined,
          linePrimitive: undefined,
          radius: undefined,
          center: undefined,
          pixelPadding: 10.0,
          angleStart: 0.0,
          angleEnd: Cesium.Math.PI,
          maxDimension: undefined,
        };

        function offsetBillboard(
          entity,
          entityPosition,
          angle,
          magnitude,
          lines,
          billboardEyeOffsets,
          labelEyeOffsets
        ) {
          const x = magnitude * Math.cos(angle);
          const y = magnitude * Math.sin(angle);

          const offset = new Cesium.Cartesian2(x, y);

          const drawingBufferWidth = scene.drawingBufferWidth;
          const drawingBufferHeight = scene.drawingBufferHeight;
          const pixelRatio = scene.pixelRatio;

          const diff = Cesium.Cartesian3.subtract(
            entityPosition,
            camera.positionWC,
            new Cesium.Cartesian3()
          );
          const distance = Cesium.Cartesian3.dot(camera.directionWC, diff);

          const dimensions = camera.frustum.getPixelDimensions(
            drawingBufferWidth,
            drawingBufferHeight,
            distance,
            pixelRatio,
            new Cesium.Cartesian2()
          );
          Cesium.Cartesian2.multiplyByScalar(
            offset,
            Cesium.Cartesian2.maximumComponent(dimensions),
            offset
          );

          let labelOffset;
          const billboardOffset = entity.billboard.eyeOffset;

          const eyeOffset = new Cesium.Cartesian3(offset.x, offset.y, 0.0);
          entity.billboard.eyeOffset = eyeOffset;
          if (Cesium.defined(entity.label)) {
            labelOffset = entity.label.eyeOffset;
            entity.label.eyeOffset = new Cesium.Cartesian3(
              offset.x,
              offset.y,
              -10.0
            );
          }

          const endPoint = Cesium.Matrix4.multiplyByPoint(
            camera.viewMatrix,
            entityPosition,
            new Cesium.Cartesian3()
          );
          Cesium.Cartesian3.add(eyeOffset, endPoint, endPoint);
          Cesium.Matrix4.multiplyByPoint(
            camera.inverseViewMatrix,
            endPoint,
            endPoint
          );
          lines.push(endPoint);

          billboardEyeOffsets.push(billboardOffset);
          labelEyeOffsets.push(labelOffset);
        }

        function starBurst(mousePosition) {
          if (Cesium.defined(starBurstState.pickedEntities)) {
            return;
          }

          const pickedObjects = scene.drillPick(mousePosition);
          if (!Cesium.defined(pickedObjects) || pickedObjects.length < 2) {
            return;
          }

          const billboardEntities = [];
          let length = pickedObjects.length;
          let i;

          for (i = 0; i < length; ++i) {
            const pickedObject = pickedObjects[i];
            if (pickedObject.primitive instanceof Cesium.Billboard) {
              billboardEntities.push(pickedObject);
            }
          }

          if (billboardEntities.length === 0) {
            return;
          }

          const pickedEntities = (starBurstState.pickedEntities = []);
          const billboardEyeOffsets = (starBurstState.billboardEyeOffsets = []);
          const labelEyeOffsets = (starBurstState.labelEyeOffsets = []);
          const lines = [];
          starBurstState.maxDimension = Number.NEGATIVE_INFINITY;

          const angleStart = starBurstState.angleStart;
          const angleEnd = starBurstState.angleEnd;

          let angle = angleStart;
          let angleIncrease;
          let magnitude;
          let magIncrease;
          let maxDimension;

          // Drill pick gets all of the entities under the mouse pointer.
          // Find the billboards and set their pixel offsets in a circle pattern.
          length = billboardEntities.length;
          i = 0;
          while (i < length) {
            let object = billboardEntities[i];
            if (pickedEntities.length === 0) {
              starBurstState.center = Cesium.Cartesian3.clone(
                object.primitive.position
              );
            }

            if (!Cesium.defined(angleIncrease)) {
              const width = object.primitive.width;
              const height = object.primitive.height;
              maxDimension =
                Math.max(width, height) * object.primitive.scale +
                starBurstState.pixelPadding;
              magnitude = maxDimension + maxDimension * 0.5;
              magIncrease = magnitude;
              angleIncrease = maxDimension / magnitude;
            }

            offsetBillboard(
              object.id,
              object.primitive.position,
              angle,
              magnitude,
              lines,
              billboardEyeOffsets,
              labelEyeOffsets
            );
            pickedEntities.push(object);

            const reflectedAngle = angleEnd - angle;
            if (
              i + 1 < length &&
              reflectedAngle - angleIncrease * 0.5 > angle + angleIncrease * 0.5
            ) {
              object = billboardEntities[++i];
              offsetBillboard(
                object.id,
                object.primitive.position,
                reflectedAngle,
                magnitude,
                lines,
                billboardEyeOffsets,
                labelEyeOffsets
              );
              pickedEntities.push(object);
            }

            angle += angleIncrease;
            if (
              reflectedAngle - angleIncrease * 0.5 <
              angle + angleIncrease * 0.5
            ) {
              magnitude += magIncrease;
              angle = angleStart;
              angleIncrease = maxDimension / magnitude;
            }

            ++i;
          }

          // Add lines from the pick center out to the translated billboard.
          const instances = [];
          length = lines.length;
          for (i = 0; i < length; ++i) {
            const pickedEntity = pickedEntities[i];
            starBurstState.maxDimension = Math.max(
              pickedEntity.primitive.width,
              pickedEntity.primitive.height,
              starBurstState.maxDimension
            );

            instances.push(
              new Cesium.GeometryInstance({
                geometry: new Cesium.SimplePolylineGeometry({
                  positions: [starBurstState.center, lines[i]],
                  arcType: Cesium.ArcType.NONE,
                  granularity: Cesium.Math.PI_OVER_FOUR,
                }),
                attributes: {
                  color: Cesium.ColorGeometryInstanceAttribute.fromColor(
                    Cesium.Color.WHITE
                  ),
                },
              })
            );
          }

          starBurstState.linePrimitive = scene.primitives.add(
            new Cesium.Primitive({
              geometryInstances: instances,
              appearance: new Cesium.PerInstanceColorAppearance({
                flat: true,
                translucent: false,
              }),
              asynchronous: false,
            })
          );

          viewer.selectedEntity = undefined;
          starBurstState.radius = magnitude + magIncrease;
        }

        function updateStarBurst(mousePosition) {
          if (!Cesium.defined(starBurstState.pickedEntities)) {
            return;
          }

          if (!starBurstState.enabled) {
            // For some reason we get a mousemove event on click, so
            // do not show a label on the first event.
            starBurstState.enabled = true;
            return;
          }

          // Remove the star burst if the mouse exits the screen space circle.
          // If the mouse is inside the circle, show the label of the billboard the mouse is hovering over.
          const screenPosition = Cesium.SceneTransforms.wgs84ToWindowCoordinates(
            scene,
            starBurstState.center
          );
          const fromCenter = Cesium.Cartesian2.subtract(
            mousePosition,
            screenPosition,
            new Cesium.Cartesian2()
          );
          const radius = starBurstState.radius;

          if (
            Cesium.Cartesian2.magnitudeSquared(fromCenter) > radius * radius ||
            fromCenter.y >
              3.0 * (starBurstState.maxDimension + starBurstState.pixelPadding)
          ) {
            undoStarBurst();
          } else {
            showLabels(mousePosition);
          }
        }

        function undoStarBurst() {
          const pickedEntities = starBurstState.pickedEntities;
          if (!Cesium.defined(pickedEntities)) {
            return;
          }

          const billboardEyeOffsets = starBurstState.billboardEyeOffsets;
          const labelEyeOffsets = starBurstState.labelEyeOffsets;

          // Reset billboard and label pixel offsets.
          // Hide overlapping labels.
          for (let i = 0; i < pickedEntities.length; ++i) {
            const entity = pickedEntities[i].id;
            entity.billboard.eyeOffset = billboardEyeOffsets[i];
            if (Cesium.defined(entity.label)) {
              entity.label.eyeOffset = labelEyeOffsets[i];
              entity.label.show = false;
            }
          }

          // Remove lines from the scene.
          // Free resources and reset state.
          scene.primitives.remove(starBurstState.linePrimitive);
          starBurstState.linePrimitive = undefined;
          starBurstState.pickedEntities = undefined;
          starBurstState.billboardEyeOffsets = undefined;
          starBurstState.labelEyeOffsets = undefined;
          starBurstState.radius = undefined;
          starBurstState.enabled = false;
        }

        let currentObject;

        function showLabels(mousePosition) {
          const pickedObjects = scene.drillPick(mousePosition);
          let pickedObject;

          if (Cesium.defined(pickedObjects)) {
            const length = pickedObjects.length;
            for (let i = 0; i < length; ++i) {
              if (pickedObjects[i].primitive instanceof Cesium.Billboard) {
                pickedObject = pickedObjects[i];
                break;
              }
            }
          }

          if (pickedObject !== currentObject) {
            if (
              Cesium.defined(pickedObject) &&
              Cesium.defined(pickedObject.id.label)
            ) {
              if (Cesium.defined(currentObject)) {
                currentObject.id.label.show = false;
              }

              currentObject = pickedObject;
              pickedObject.id.label.show = true;
            } else if (Cesium.defined(currentObject)) {
              currentObject.id.label.show = false;
              currentObject = undefined;
            }
          }
        }

        //Sandcastle_End
        Sandcastle.finishedLoading();
      }
      if (typeof Cesium !== "undefined") {
        window.startupCalled = true;
        startup(Cesium);
      }
    </script>
  </body>
</html>
